/**
 * ======================================================================
 * 插件作用：实现在元素的 class 属性中直接合并 windicss 原子类
 * ----------------------------------------------------------------------
 * Author: 曹操<pgcao@qq.com>(https://gitee.com/pgcao) License MIT
 * ======================================================================
 * see https://gitee.com/pgcao/windicss-elements-apply
 */

export default (options = {}) => {
  const PLUGIN_NAME = 'vite-plugin-windicss-elements-apply';

  const { enabled = true } = options;
  if (!enabled) {
    return {
      name: PLUGIN_NAME
    };
  }

  const regexClassSplitter = /[\s'"`{}]/g;
  const regexClassCheck1 = /^!?[a-z\d@<>.+-](?:\([\w,.%#\(\)+-]*\)|[\w:/\\,%#\[\].$-])*$/;
  const regexClassCheck2 = /[a-z].*[\w)\]]$/;
  const regexClassChecks = [regexClassCheck1, regexClassCheck2];

  const formatReg = /<([\w]+)\s*[^>]*(?=class\s*?=\s*?((['"])*[^'"]*))[^>]*\/?>/gi;
  const applyReg = /^@([a-z]+)(?:([a-z0-9-_]+))?(\s*\(([^)]*)\))?/i;

  async function hashCode(str) {
    return new Promise(resolve => {
      str = str.replace(/\s*\r?\n/g, '');
      let hash = 5381;
      let i = str.length;
      while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
      let hashStr = (hash >>> 0).toString(36);
      resolve(hashStr);
    });
  }

  async function replaceAsync(str, regex, asyncFn) {
    const promises = [];
    str.replace(regex, (match, ...args) => {
      const promise = asyncFn(match, ...args);
      promises.push(promise);
    });
    const data = await Promise.all(promises);
    return str.replace(regex, () => data.shift());
  }

  function validClassName(i) {
    return regexClassChecks.every(r => i.length > 2 && i.match(r));
  }

  async function supportStyleName(source, id, isUpdate) {
    const styleKey = 'css';
    const styleCode = new Map();
    const styleScopedCode = new Map();
    const classNum_key = await hashCode(id);

    let classIndex = 0;

    // 取出带有class属性的元素标签头
    const formatFun = async (elements, tags, classValues, marks) => {
      if (!classValues.includes('@apply')) return elements;

      const [classNames, ...Others] = classValues.substring(1).split(';');
      const ApplyOther_after = Others.join(' ');

      const classNamesArr = classNames.split(regexClassSplitter).filter(validClassName);
      if (!classNamesArr.length) return elements;

      const applyArr = applyReg.exec(classNames) || [];
      if (applyArr.length < 5 || applyArr[1] !== 'apply') {
        return elements;
      }

      const [ApplyName, ApplyOther_befor] = [applyArr[2], applyArr[4]];
      const newClassNames = styleKey + '-' + (ApplyName ? ApplyName.replace('-', '') : classNum_key + '-' + classIndex);

      classNamesArr.shift();
      if (classNamesArr.length) {
        const validClassNames = classNamesArr.join(' ');
        ApplyName ? styleCode.set(newClassNames, `@apply ${validClassNames};`) : styleScopedCode.set(newClassNames, `@apply ${validClassNames};`);
      }

      const newClassValues = marks + (ApplyOther_befor ? ApplyOther_befor + ' ' : '') + newClassNames + (ApplyOther_after ? ' ' + ApplyOther_after : '');

      classIndex++;

      return elements.replace(classValues, newClassValues);
    };

    source = await replaceAsync(source.trimStart(), formatReg, formatFun);

    if (styleCode.size) {
      const styleStr = new Array();
      for (const [key, val] of styleCode) {
        styleStr.push(`.${key}{\n${val};\n}`);
      }
      source = `<style>${styleStr.join('\n')}</style>\r\n` + source;
    }

    if (styleScopedCode.size) {
      const styleScopedStr = new Array();
      for (const [key, val] of styleScopedCode) {
        styleScopedStr.push(`.${key}{\n${val};\n}`);
      }
      source = `<style scoped>${styleScopedStr.join('\n')}</style>\r\n` + source;
    }

    return source;
  }

  return [
    {
      name: PLUGIN_NAME,
      enforce: 'pre',
      // 请求模块时处理文件
      async transform(code, id) {
        if (!/\.vue$/.test(id)) {
          return null;
        }
        try {
          return await supportStyleName(code, id, false);
        } catch (e) {
          this.error(e);
        }
        return null;
      },

      // 执行自定义 HMR 更新处理
      async handleHotUpdate(ctx) {
        if (!/\.vue$/.test(ctx.file)) {
          return null;
        }
        const defaultRead = ctx.read;
        ctx.read = async function () {
          return await supportStyleName(await defaultRead(), ctx.file, true);
        };
      }
    },
    {
      name: `${PLUGIN_NAME}:css`,
      enforce: 'post',
      // 去掉 sourcemap
      async transform(source, id) {
        if (!/type=style/i.test(id)) {
          return;
        }
        return {
          code: source,
          map: { mappings: '' }
        };
      }
    }
  ];
};
